// Copyright (c) 2023 Presto Labs Pte. Ltd.
// Author: sangyul

#include <algorithm>
#include "coin2/strategy/linear_model/focus3.h"

namespace coin2::strategy::linear_model {

void LmSubStrategy::UpdateSignal3(
    int64_t timestamp,
    int64_t* first_feed_time,
    double midret_pred,
    std::optional<double> midret_pred_sell,
    std::optional<double> midtomed_pred,
    std::optional<double> midtomax_pred,
    std::optional<double> midtomin_pred,
    std::optional<double> residual_pred,
    OrderExecutorSystem* oe,
    PredictionInterval* pred_interval,
    AggregatedSizer* asizer,
    double curr_shared_stamp) {
  if (!bbo) return;
  if (!pholder) return;
  CHECK(oe);
  if (!oe->HasProduct(*pholder)) return;
  if (!oe->HasPosition(*pholder)) return;

  if (focus_config.use_midtomed_signal()) {
    midret_pred = *midtomed_pred;
  }

  if (focus_config.use_residual_signal()) {
    CHECK(!focus_config.use_midtomed_signal());
    midret_pred += *residual_pred * focus_config.residual_signal_multiplier();
  }

  if (focus_config.buffer_mode() && is_buffer_symbol) {
    CHECK(focus_config.use_reserve() == 0);
    OnBufferSignal(timestamp, first_feed_time, midret_pred, 0, oe, pred_interval, asizer);
    return;
  }

  if (last_logged_ts_ == 0L) last_logged_ts_ = timestamp;
  if (last_logged_ts_ + 120'000'000'000L < timestamp) {
    if (focus_config.use_sell_model()) {
      LOG(INFO) << symbol->symbol_string() << ": buy_signal=" << midret_pred << ", sell_signal=" << *midret_pred_sell;
    } else {
      LOG(INFO) << symbol->symbol_string() << ": signal=" << midret_pred;
    }
    last_logged_ts_ = timestamp;
  }

  const auto& pi = pholder->product_info();
  double contract_value = pi.contract_value() > 0? pi.contract_value(): 1.0;

  // set params
  // recommend to set to satisfy: MAP > 2 * MVAP * sum_{i=3}^n V_i'' / min(1/sum(abs(V_i'')), concen)
  int horiz_type = focus_config.horiz_type(); // 0 for sec, 1 for bps, 2 for ysqbps, 3 for ybebps
  int is_y_neut = focus_config.is_y_neut(); // 0 for s, uneut, 1 for neut
  int is_y_laplace = focus_config.is_y_laplace();
  int is_y_ub = focus_config.is_y_ub(); // 0 for None, 1 for ub_sec, 2 for ub_bps
  int num_symbols = asizer->num_focus_index;
  double stack = focus_config.stack();
  double mvaq = focus_config.max_virtual_abspos_quote();
  double gamma = focus_config.max_concentration();
  double lot_size_quote = 2.0 * mvaq * gamma / stack;

  double lot_size_qty = util::GetQtyFromQuoteSafe(
    *pholder,
    price_window_.last_element(),
    price_window_.first_element(),
    lot_size_quote);

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // get virtual_target_norpos_curr from signal

  pred_interval->UpdateSignal(timestamp, midret_pred);
  if (focus_config.use_zscore_signal()) {
    double ret_mean, ret_std;
    if (!pred_interval->GetMeanStd(&ret_mean, &ret_std, focus_config.zscore_signal_warmup()) || ret_std <= 0.0) return;
    // midret_pred = (midret_pred - ret_mean);
    midret_pred = (midret_pred - ret_mean) / ret_std;
  }

  if (*first_feed_time == 0L) *first_feed_time = timestamp;
  if (timestamp - *first_feed_time < 30e9) return;
  
  // sig -> target_pos_curr
  double sig = midret_pred; // in [-1, 1] for soy, tpos model.
  sig -= focus_config.signal_bias(); 
  sig *= focus_config.signal_multiplier();
  if (focus_config.use_sell_model()) {
    *midret_pred_sell *= focus_config.signal_multiplier();
  }

  double virtual_target_norpos_curr = 0.0;
  if (focus_config.cal_tpos_method() == 0) {
    if (sig >= 0) {
      virtual_target_norpos_curr = (sig - focus_config.threshold_tpos()) / focus_config.lean_tpos();
      virtual_target_norpos_curr = std::clamp(virtual_target_norpos_curr, 0.0, 1.0);
    } else {
      virtual_target_norpos_curr = (sig + focus_config.threshold_tpos()) / focus_config.lean_tpos();
      virtual_target_norpos_curr = std::clamp(virtual_target_norpos_curr, -1.0, 0.0);
    }
  } else {
    CHECK(false) << "invalid cal_tpos_method";
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // get weight
  double weight_pivot = 0.0;
  double w_laplace = 0.0;
  int64_t pivot_time = 0;
  int64_t debug_first_time = 0L;
  double d_move = 0.0;

  if (horiz_type == 0) { // for sec
    int64_t dt = 0L;
    if (last_pred_time != 0L) {
      dt = timestamp - last_pred_time;
    } else {
      debug_first_time = timestamp;
    }
    last_pred_time = timestamp;

    weight_pivot = static_cast<double>(dt) / static_cast<double>(focus_config.prediction_window_sec() * 1e9);
    w_laplace = weight_pivot;
    pivot_time = timestamp;
  } else {
    if (horiz_type == 1 || horiz_type == 2) { // for bps,  ysqbps (sub_stamp)
      d_move = curr_sub_stamp - last_pred_stamp;
      pivot_time = static_cast<int64_t>(curr_sub_stamp);
      last_pred_stamp = curr_sub_stamp;
    } else if (horiz_type == 3) { // for ybebps (shared_stamp)
      d_move = curr_shared_stamp - last_pred_stamp;
      pivot_time = static_cast<int64_t>(curr_shared_stamp);
      last_pred_stamp = curr_shared_stamp;
    } else {
      CHECK(false) << "invalid horiz_type";
    }

    weight_pivot = d_move / static_cast<double>(focus_config.prediction_window_sec() * 1e9);
    w_laplace = weight_pivot;
    bp_volatility_window.Update(timestamp, d_move / 1e9);
    if (focus_config.normalize_bp_volatility() == 1) {
      weight_pivot *= std::min(focus_config.dt_max_pysim() / (bp_volatility_window.sum() + 1e-12), 1.0);
    }
  }

  double liquidity_penalty = 1.0;
  double liquidity_sum = 0.0;
  if (focus_config.liquidity_constraint() > 0.0) {
    liquidity_.UpdateTimeOnly(timestamp);
    liquidity_sum = liquidity_.sum();
    if (pi.is_inverse()) {
      liquidity_sum /= bbo->askp;
    }
    double ub_by_liquidity = contract_value * liquidity_sum * focus_config.liquidity_constraint();
    liquidity_penalty = std::min(ub_by_liquidity / (mvaq * gamma + 1e-12), 1.0);
    // TODO(Sangyul): able to optimize mvaq * gamma, for large cu (>30)
  }

  weight_pivot = std::min(weight_pivot, 60.0 / focus_config.prediction_window_sec());
  weight_pivot *= liquidity_penalty;

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // get virtual_target_norpos_ma
  if (is_y_laplace == 1) {
    // use "weight_pivot" instead of "1-exp(-weight_pivot)", since weight_pivot is small enough
    virtual_target_norpos_ma *= std::exp(-w_laplace);
    virtual_target_norpos_ma += weight_pivot * virtual_target_norpos_curr;

    if (is_y_ub == 1) {
      // upper bound: 10800s
      dmove_interval.Update(timestamp, d_move);
      double decay_weight = dmove_interval.sum() / static_cast<double>(focus_config.prediction_window_sec() * 1e9);

      vtpos_for_ub.UpdatePosition(timestamp, weight_pivot * virtual_target_norpos_curr);
      double curr_vtpos_for_ub_sum = 0.0;
      if (!vtpos_for_ub.GetSum(&curr_vtpos_for_ub_sum, false)) {
        return;
      }
      double poped_sum = prev_vtpos_for_ub_sum + (weight_pivot * virtual_target_norpos_curr) - curr_vtpos_for_ub_sum;
      prev_vtpos_for_ub_sum = curr_vtpos_for_ub_sum;

      virtual_target_norpos_ma -= std::exp(-decay_weight) * poped_sum;
    }
  } else {
    virtual_target_norpos_ma = 0.0;
    if (is_y_ub == 0) {
      virtual_target_norpos_interval.UpdatePosition(pivot_time, weight_pivot * virtual_target_norpos_curr);
      if (!virtual_target_norpos_interval.GetSum(&virtual_target_norpos_ma, false)) {
        return;
      }
    } else if (is_y_ub == 1) {
      vtpos_doubleinterval.UpdatePosition(
        pivot_time, 
        timestamp,
        weight_pivot * virtual_target_norpos_curr);
      if (!vtpos_doubleinterval.GetSum(&virtual_target_norpos_ma, false)) {
        return;
      }
    }
  }
  virtual_target_norpos_ma = std::clamp(virtual_target_norpos_ma, -1.0, 1.0); // just for sanity

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // hedge on universe
  (asizer->tpos_ma_for_uhedge).Update((asizer->symbol_index)[symbol], virtual_target_norpos_ma);
  double virutal_target_norpos_ma_mean = (asizer->tpos_ma_for_uhedge).get_sum() / num_symbols;
  double virutal_target_norpos_uhedged = virtual_target_norpos_ma;
  virutal_target_norpos_uhedged -= focus_config.hedge_ratio_universe() * virutal_target_norpos_ma_mean;

  double virutal_target_norpos_scaled = virutal_target_norpos_uhedged * focus_config.multiplier_tpos();
  // assert that virutal_target_norpos_scaled in [-1,1]
  if (focus_config.clip_method_after_universe_hedge() == 0) {
    virutal_target_norpos_scaled = std::clamp(virutal_target_norpos_scaled, -1.0, 1.0);
  } else if (focus_config.clip_method_after_universe_hedge() == 1) { // will be deprecated
    virutal_target_norpos_scaled *= (1.0 / (2.0 - 2.0 / num_symbols));
    virutal_target_norpos_scaled = std::clamp(virutal_target_norpos_scaled, -1.0, 1.0); // just for sanity
  } else {
    CHECK(false) << "invalid clip_method_after_universe_hedge";
  }
  (asizer->vtpos_scaled_online).Update((asizer->symbol_index)[symbol], virutal_target_norpos_scaled);
  double v_tmp = virutal_target_norpos_scaled;
  if (is_buffer_symbol) {
    v_tmp -= (asizer->vtpos_scaled_online).get_sum() / 2.0 * focus_config.hedge_ratio_neut();
  }
  (asizer->vtpos_scaled_for_capa).Update((asizer->symbol_index)[symbol], v_tmp);
  
  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // get actual_target_stk
  double capacity_quote = mvaq * std::min(1.0 / ((asizer->vtpos_scaled_for_capa).get_abssum() + 1e-12), gamma);
  double virtual_target_quote = virutal_target_norpos_scaled * capacity_quote;

  double curr_pos = GetCurrPos(oe);
  double curr_pos_quote = GetQuoteFromPq(
      *pholder,
      bbo->askp,
      curr_pos);
  asizer->UpdatePosition(symbol, curr_pos_quote);

  // get quote_sum_for_nhedge
  if (is_buffer_symbol) {
    (asizer->mixed_quote_for_nhedge).Update((asizer->symbol_index)[symbol], virtual_target_quote);
  } else {
    (asizer->mixed_quote_for_nhedge).Update((asizer->symbol_index)[symbol], curr_pos_quote);
  }
  double quote_sum_for_nhedge = (asizer->mixed_quote_for_nhedge).get_sum();
  
  // get actual_target_quote by neutralize
  double actual_target_quote = virtual_target_quote;
  if (is_buffer_symbol) {
    actual_target_quote -= quote_sum_for_nhedge / 2.0 * focus_config.hedge_ratio_neut();
  } 
  
  // TODO(sangyul) insert your custom hedging logic. e.g, PCA.

  double actual_target_pos = util::GetQtyFromQuoteSafe(
    *pholder,
    price_window_.last_element(),
    price_window_.first_element(),
    actual_target_quote);

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // tune max, min pos, according to focus2 logics.
  double min_quote = - capacity_quote;
  double max_quote = capacity_quote;
  double reserve_quote = GetQuoteFromPq(*pholder, bbo->askp, reserve);

  if (is_y_neut == 1 && is_buffer_symbol) {
    min_quote *= num_symbols / 2.0;
    max_quote *= num_symbols / 2.0;
  }
  
  bool buy_block = false, sell_block = false;

  if (asizer->symbols_clear.count(symbol) > 0) {
    min_quote = reserve_quote;
    max_quote = reserve_quote;
  }

  double max_pos = util::GetQtyFromQuoteSafe(
      *pholder,
      price_window_.last_element(),
      price_window_.first_element(),
      max_quote);
  double min_pos = util::GetQtyFromQuoteSafe(
      *pholder,
      price_window_.last_element(),
      price_window_.first_element(),
      min_quote);

  auto bs = asizer->GetRemainingBuySellQuote();
  if (focus_config.use_aggregated_pos()) {
    double remain_buy_qty = std::max(0., GetQtyFromQuote(*pholder, bbo->askp, bs.first));
    double remain_sell_qty = std::max(0., GetQtyFromQuote(*pholder, bbo->askp, bs.second));
    max_pos = std::min(curr_pos + remain_buy_qty, max_pos);
    min_pos = std::max(curr_pos - remain_sell_qty, min_pos);
  }

  double pr_exceed_ratio = 0.0;
  if (focus_config.pr_constraint() > 0.0) {
    buy_turnover_.UpdateTimeOnly(timestamp); // default of pr_window_in_sec: 600s
    sell_turnover_.UpdateTimeOnly(timestamp);
    market_turnover_.UpdateTimeOnly(timestamp);
    double max_pr = focus_config.pr_constraint();
    double market_turnover = market_turnover_.sum();
    double turnover = buy_turnover_.sum() + sell_turnover_.sum();
    bool pr_exceed = turnover > (market_turnover * max_pr);
    buy_block |= pr_exceed;
    sell_block |= pr_exceed;
    pr_exceed_ratio = turnover / (market_turnover * max_pr + 1e-12);
  }

  trading_pnls_.UpdateTimeOnly(timestamp);
  holding_pnls_.UpdateTimeOnly(timestamp);
  double midp = 0.5 * (bbo->askp + bbo->bidp);
  double holding_pnl = curr_pos * midp;
  if (holding_pnls_.num_popped() > 0) {
    holding_pnl -= holding_pnls_.last_popped();
  }
  double pnl_q = 0;
  if (trading_pnls_.num_popped() > 0) {
    pnl_q = (trading_pnls_.sum() + holding_pnl) / midp;
  }
  if (pnl_q < 0) {
    min_pos -= focus_config.loss_penalty_ratio() * pnl_q;
    max_pos += focus_config.loss_penalty_ratio() * pnl_q;
  }

  // limit exposure
  if (focus_config.use_aggregated_pos()) {
    buy_block |= bs.first <= 0;
    sell_block |= bs.second <= 0;
  }

  // limit abspos quote
  if (focus_config.max_abspos_quote() > 0) {
    auto bs2 = asizer->GetRemainingAbsBuySellQuote(symbol);
    buy_block |= bs2.first <= 0;
    sell_block |= bs2.second <= 0;
  }

  double buy_fill_ratio = 0.0;
  double sell_fill_ratio = 0.0;
  if (focus_config.fill_cooldown_constraint() > 0) {
    buy_filled_.UpdateTimeOnly(timestamp);
    sell_filled_.UpdateTimeOnly(timestamp);
    buy_block |= buy_filled_.sum() > lot_size_qty * focus_config.fill_cooldown_constraint();
    sell_block |= sell_filled_.sum() > lot_size_qty * focus_config.fill_cooldown_constraint();
    buy_fill_ratio = buy_filled_.sum() / (lot_size_qty * focus_config.fill_cooldown_constraint() + 1e-12);
    sell_fill_ratio = sell_filled_.sum() / (lot_size_qty * focus_config.fill_cooldown_constraint() + 1e-12);
  }

  actual_target_pos = std::clamp(actual_target_pos, min_pos, max_pos);

  ////////////////////////////////////////////////////////////////////////////////////////////////////
  // tune and submit order.
  double min_order_qty = util::GetQtyFromQuoteSafe(
    *pholder,
    price_window_.last_element(),
    price_window_.first_element(),
    focus_config.min_order_quote());
  
  double actual_target_stk = actual_target_pos / lot_size_qty; // actual_target_stk in [-s/2, s/2] for not buffer symbols.
  double current_stack = curr_pos / lot_size_qty;
  if (actual_target_stk <= current_stack + 1.0) {
    buy_block = true;
  }
  if (actual_target_stk >= current_stack - 1.0) {
    sell_block = true;
  }

  double dstk = std::abs(actual_target_stk - current_stack);
  double dstk_tolerance_min = focus_config.dstk_tolerance_min();
  double dstk_tolerance_max = focus_config.dstk_tolerance_max();
  double dstk_tolerance = (dstk - dstk_tolerance_min) / (dstk_tolerance_max - dstk_tolerance_min);
  double posting_dist_bbo = focus_config.posting_dist_bbo() * std::clamp(1.0 - dstk_tolerance, 0.0, 1.0);
  bool posting_add_tick = (dstk > dstk_tolerance_max);
  
  double lot_size_multiplyer = std::clamp(dstk, 1.0, focus_config.lot_size_multiplyer_ub());
  if (is_y_neut == 1 && is_buffer_symbol) {
    lot_size_multiplyer = std::clamp(dstk, 1.0, focus_config.lot_size_multiplyer_ub_buffer());
  }

  double remain_buy_qty = 0.0;
  double remain_sell_qty = 0.0;
  if (focus_config.book_constraint() > 0.0) {
    double ratio = focus_config.book_constraint();

    ask_bbo_.UpdateTimeOnly(timestamp);
    bid_bbo_.UpdateTimeOnly(timestamp);
    double ask_bbo = ask_bbo_.average();
    double bid_bbo = bid_bbo_.average();
    auto& tickprc = pholder->product_info().tick_price();
    double book_mult = tickprc.Infer2bpBookMult(bbo->askp);

    remain_buy_qty = std::max(0.0, bid_bbo * book_mult * ratio / bbo->askp);
    remain_sell_qty = std::max(0.0, ask_bbo * book_mult * ratio / bbo->askp);

    if (!buy_block) {
      lot_size_multiplyer = std::min(lot_size_multiplyer, remain_buy_qty / lot_size_qty);
    }
    if (!sell_block) {
      lot_size_multiplyer = std::min(lot_size_multiplyer, remain_sell_qty / lot_size_qty);
    }
  }

  bool need_taker = false;
  // TODO(sangyul): for taker order
  // CHECK(focus_config.need_taker_stks() > 1.0);
  // if (std::abs(target_stk - current_stack) > focus_config.need_taker_stks()) {
  //   need_taker = true;
  // }
  
  double maker_order_qty = lot_size_qty * lot_size_multiplyer;
  oe->AdjustSize(*pholder, min_pos, max_pos, maker_order_qty, min_order_qty);

  if (focus_config.print_debug_log_type() == 1) {
    if (last_debug_logged_ts_ == 0L) {
      last_debug_logged_ts_ = timestamp;
    }
    if (last_debug_logged_ts_ + focus_config.debug_log_sec() * 1'000'000'000L < timestamp) {
      LOG(INFO) << "lsy_log: symbols:" << symbol->symbol_string();
      LOG(INFO) << "lsy_log: time:" << timestamp;
      LOG(INFO) << "lsy_log: current_stack:" << current_stack;
      LOG(INFO) << "lsy_log: actual_target_stk:" << actual_target_stk;
      LOG(INFO) << "lsy_log: actual_target_pos:" << actual_target_pos;
      LOG(INFO) << "lsy_log: actual_target_quote:" << actual_target_quote; 
      LOG(INFO) << "lsy_log: virtual_target_quote:" << virtual_target_quote;
      LOG(INFO) << "lsy_log: virutal_target_norpos_uhedged:" << virutal_target_norpos_uhedged;
      LOG(INFO) << "lsy_log: virtual_target_norpos_ma:" << virtual_target_norpos_ma;
      LOG(INFO) << "lsy_log: virtual_target_norpos_curr:" << virtual_target_norpos_curr;
      LOG(INFO) << "lsy_log: quote_sum_for_nhedge:" << quote_sum_for_nhedge;
      LOG(INFO) << "lsy_log: capacity_quote:" << capacity_quote;
      LOG(INFO) << "lsy_log: min_stk:" << min_pos / lot_size_qty;
      LOG(INFO) << "lsy_log: max_stk:" << max_pos / lot_size_qty;
      LOG(INFO) << "lsy_log: lot_size_qty:" << lot_size_qty;
      LOG(INFO) << "lsy_log: maker_order_stk:" << maker_order_qty / lot_size_qty;
      LOG(INFO) << "lsy_log: remain_buy_stk:" << remain_buy_qty / lot_size_qty;
      LOG(INFO) << "lsy_log: remain_sell_stk:" << remain_sell_qty / lot_size_qty;
      LOG(INFO) << "lsy_log: buy_block:" << (buy_block? 1.0: 0.0);
      LOG(INFO) << "lsy_log: sell_block:" << (sell_block? 1.0: 0.0);
      LOG(INFO) << "lsy_log: buy_fill_ratio:" << buy_fill_ratio;
      LOG(INFO) << "lsy_log: sell_fill_ratio:" << sell_fill_ratio;
      LOG(INFO) << "lsy_log: pr_exceed_ratio:" << pr_exceed_ratio;
      LOG(INFO) << "lsy_log: liquidity_sum:" << liquidity_sum;   
      LOG(INFO) << "lsy_log: is_buffer_symbol:" << is_buffer_symbol;
      LOG(INFO) << "lsy_log: askp:" << bbo->askp;
      LOG(INFO) << "lsy_log: bidp:" << bbo->bidp;
      LOG(INFO) << "lsy_log: trading_pnl:" << trading_pnls_.sum();
      LOG(INFO) << "lsy_log: holding_pnl:" << holding_pnl;
      LOG(INFO) << "lsy_log: curr_sub_stamp:" << curr_sub_stamp;
      LOG(INFO) << "lsy_log: curr_shared_stamp:" << curr_shared_stamp;
      LOG(INFO) << "lsy_log: debug_first_t:" << debug_first_time;
      last_debug_logged_ts_ = timestamp;
    }
  }

  ManageMmOrder6(
      oe,
      *pholder,
      *bbo,
      timestamp,

      &bom,
      leverage_rate,

      midtomax_pred,
      midtomin_pred,

      buy_block,
      sell_block,

      need_taker,
      maker_order_qty,
      lot_size_qty,
      posting_dist_bbo,
      posting_add_tick,

      focus_config);
  oe->onPeriodicOrderCheck(*pholder, timestamp);
}


void LmFocusLogic3::onFeed(
    const Symbol* symbol,
    const FeedUpdate& upd,
    const ::fastfeature::FeedMessage& feed_msg,
    LinearModel* linear_model,
    int64_t* lm_feed_id,
    const int64_t* global_feed_id,
    LinearModelDelegateProto* delegate,
    bool simulation) {
  if (strats_.count(symbol) > 0) {
    strats_.at(symbol)->UpdateProduct(product_cache_, upd.product(), upd.timestamp());
    if (feed_msg.has_book()
        && feed_msg.book().ask_size() > 0
        && feed_msg.book().bid_size() > 0) {
      auto ask0 = feed_msg.book().ask(0);
      auto bid0 = feed_msg.book().bid(0);
      auto bbo = Bbo {
          feed_msg.fetched_time(),
          feed_msg.exchange_time(),
          ask0.price_float(),
          bid0.price_float(),
      };
      strats_.at(symbol)->UpdateBbo(bbo);

      double ask0_pq = ask0.price_float() * ask0.qty_float();
      double bid0_pq = bid0.price_float() * bid0.qty_float();
      strats_.at(symbol)->UpdateBboPq(ask0_pq, bid0_pq, feed_msg.fetched_time());

      strats_.at(symbol)->ManageExecutorConfig2(upd.timestamp(), oe_);

      UpdateCurrTime(symbol);
    }
    if (feed_msg.feed_type() == ::fastfeature::TRADE_FEED)
      strats_.at(symbol)->UpdateTrade(
          feed_msg.trade().price_float(),
          feed_msg.trade().qty_float(),
          feed_msg.fetched_time());
  }

  // LOG(INFO) << linear_model->GetAllFeatures()->DebugString();
  if (asizer_.symbols_clear.count(symbol) > 0) {
    CHECK(strats_.count(symbol) > 0);
    strats_[symbol]->ClearPositionWoSignal(
        upd.timestamp(),
        oe_,
        &asizer_);
    return;
  }
  delegate->ClearProto();
  lholder_.TriggerModelWithLatencyRecording(linear_model, nullptr);

  if (delegate->response_.buy_model().empty()) {
    if (strats_.count(symbol) > 0) {
      if (last_logged_tss_[symbol] == 0L) last_logged_tss_[symbol] = feed_msg.fetched_time();
      if (last_logged_tss_[symbol] + 120'000'000'000L < feed_msg.fetched_time()) {
        LOG(INFO) << symbol->symbol_string() << ": buy_model is empty";
        last_logged_tss_[symbol] = feed_msg.fetched_time();
      }
    }
    return;
  }
  auto signal_resp = delegate->response_.buy_model().Get(0);
  double buymodel_signal = 0.0, sellmodel_signal = 0.0;
  int n_models = delegate->response_.buy_model().size();
  for (int i = 0; i < n_models; i++) {
    buymodel_signal += delegate->response_.buy_model().Get(i).signal();
  }
  buymodel_signal /= n_models;

  // use_nth_model_only begins from 1; if zero, ensemble as above (default).
  int32_t model_idx = lm_product_config_.focus_config().use_nth_model_only();
  if (model_idx > 0) {
    CHECK_LE(model_idx, n_models);
    buymodel_signal = delegate->response_.buy_model().Get(model_idx - 1).signal();
  }

  if (lm_product_config_.focus_config().use_sell_model()) {
    n_models = delegate->response_.sell_model().size();
    for (int i = 0; i < n_models; i++) {
      sellmodel_signal += delegate->response_.sell_model().Get(i).signal();
    }
    sellmodel_signal /= n_models;
  }

  std::optional<double> midtomed_pred;
  if (!delegate->response_.midtomed_model().empty()) {
    midtomed_pred = delegate->response_.midtomed_model().Get(0).signal();
  }

  std::optional<double> midtomax_pred;
  if (!delegate->response_.midtomax_model().empty()) {
    midtomax_pred = delegate->response_.midtomax_model().Get(0).signal();
  }

  std::optional<double> midtomin_pred;
  if (!delegate->response_.midtomin_model().empty()) {
    midtomin_pred = delegate->response_.midtomin_model().Get(0).signal();
  }

  std::optional<double> residual_pred;
  if (!delegate->response_.residual_model().empty()) {
    double resmodel_signal = 0.0;
    int n_residual_models = delegate->response_.residual_model().size();
    for (int i = 0; i < n_residual_models; i++) {
      resmodel_signal += delegate->response_.residual_model().Get(i).signal();
    }
    resmodel_signal /= n_residual_models;
    residual_pred = resmodel_signal;
  }

  auto focus_symbolstr = signal_resp.symbol();
  const Symbol* focus_symbol = linear_model->GetFeatureManager()
      ->GetSymbolManager()->FromSymbolString(focus_symbolstr);

  if (asizer_.symbols_clear.count(focus_symbol) == 0 && strats_.count(focus_symbol) > 0) {
    strats_[focus_symbol]->UpdateSignal3(
        upd.timestamp(),
        &first_feed_time_,
        buymodel_signal,
        sellmodel_signal,
        midtomed_pred,
        midtomax_pred,
        midtomin_pred,
        residual_pred,
        oe_,
        &pred_interval_,
        &asizer_,
        curr_shared_stamp);
    if (!simulation) {
      strats_[focus_symbol]->LogggingWarmupVariableFocus3(upd.timestamp());
    }
  }
  if (!simulation) {
    lholder_.LogSignal(upd.timestamp(), delegate);
  }
}

void LmFocusLogic3::UpdateCurrTime(const Symbol* symbol) {
  double midp_move = strats_.at(symbol)->GetMidpMove();

  int horiz_type = lm_product_config_.focus_config().horiz_type();
  if (horiz_type == 1) {
    strats_.at(symbol)->UpdateCurrSubStamp(std::abs(midp_move * 1e13));
  } else if (horiz_type == 2) {
    strats_.at(symbol)->UpdateCurrSubStamp(std::abs(midp_move * 1e4) * std::abs(midp_move * 1e4) / 2.0 * 1e9);
  } else if (horiz_type == 3) {
    if (strats_.at(symbol)->is_buffer_symbol) {
      curr_shared_stamp += std::abs(midp_move * 1e13);
    }
  }
}

}  // namespace coin2::strategy::linear_model
